Skip to content

feat: workflow v0.14.1 — security-audit + provenance + GitLab auth#414

Merged
intel352 merged 7 commits into
mainfrom
feat/v0.14.1-security-audit
Apr 19, 2026
Merged

feat: workflow v0.14.1 — security-audit + provenance + GitLab auth#414
intel352 merged 7 commits into
mainfrom
feat/v0.14.1-security-audit

Conversation

@intel352

Copy link
Copy Markdown
Contributor

Summary

Follow-up release addressing three features deferred from v0.14.0:

  • T34 — wfctl build audit (a944096): 6 supply-chain security checks + --strict flag. Checks hardening flag, dockerfile without SBOM/provenance, registries without retention, plugins without lockfile, unset env-var auth tokens, local environments that disable hardening.
  • T33 — BuildKit provenance (b9037a2): when ci.build.security.hardened=true, wfctl build image now appends --provenance=mode=max --sbom=true to the docker build invocation. Warns if DOCKER_BUILDKIT=1 is not set.
  • T31 — GitLab Container Registry (73f19cf): plugins/registry-gitlab now implements Login/Push/Prune. Supports CI_JOB_TOKEN auth for GitLab CI contexts and PAT-based auth elsewhere.

CHANGELOG updated with ## v0.14.1 section.

Diff

  • cmd/wfctl/build_security_audit.go + test — new audit command
  • cmd/wfctl/build.go — wired wfctl build audit subcommand
  • cmd/wfctl/build_image.go + test — provenance/sbom args when hardened
  • plugins/registry-gitlab/plugin.go + test — GitLab CR provider

Test plan

  • go test ./cmd/wfctl/... ./plugins/registry-gitlab/... -count=1 — all green locally (21 new tests)
  • golangci-lint run --timeout=10m — zero issues locally
  • CI green
  • Copilot review addressed

Related

🤖 Generated with Claude Code

intel352 and others added 3 commits April 19, 2026 01:36
New cmd/wfctl/build_security_audit.go implements runBuildSecurityAudit
with six checks: hardened flag, dockerfile sbom/provenance, registry
retention, plugins lockfile, auth env vars, and local-env hardening
override (NOTE only). Wired as `wfctl build audit`. --strict exits 1
when any WARN is present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When ci.build.security.hardened=true, buildWithDockerfile appends
--provenance=mode=max and --sbom=true to the docker build invocation.
Emits a warning when DOCKER_BUILDKIT!=1 to flag that BuildKit is
required for provenance to actually work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the stub with a real implementation: Login uses gitlab-ci-token
in CI context (CI_JOB_TOKEN), oauth2 otherwise. Push delegates to
docker push. Prune calls the GitLab API to delete tags beyond
retention.keep_latest. Add 7 dry-run tests mirroring DO/GHCR patterns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 19, 2026 05:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds v0.14.1 follow-up release features focused on supply-chain hardening and registry support, including a new build security audit command, BuildKit provenance/SBOM flags, and a GitLab Container Registry provider implementation.

Changes:

  • Added wfctl build audit (with --strict) to scan configs for supply-chain security issues.
  • Updated wfctl build image to append BuildKit provenance + SBOM flags when ci.build.security.hardened=true.
  • Implemented GitLab registry provider Login/Push/Prune and added corresponding tests; updated CHANGELOG for v0.14.1.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
cmd/wfctl/build_security_audit.go Implements the new wfctl build audit command and the six audit checks + strict mode.
cmd/wfctl/build_security_audit_test.go Adds unit tests covering audit findings and --strict exit behavior.
cmd/wfctl/build_image.go Appends provenance/SBOM flags to docker builds when hardened is enabled.
cmd/wfctl/build_image_test.go Tests for provenance/SBOM arg behavior under hardened vs non-hardened.
cmd/wfctl/build.go Wires audit into wfctl build subcommand dispatch.
plugins/registry-gitlab/plugin.go Replaces stub with GitLab registry provider implementation (Login/Push/Prune).
plugins/registry-gitlab/plugin_test.go Adds tests for GitLab provider dry-run output and auth error handling.
CHANGELOG.md Documents v0.14.1 additions for audit, provenance, and GitLab registry support.

Comment thread plugins/registry-gitlab/plugin.go Outdated
Comment on lines +168 to +171
tagsURL := fmt.Sprintf("%s/api/v4/projects/%s/registry/repositories/%d/tags", baseURL, encodedProject, repo.ID)
tags, err := glGetJSON[[]glRepoTag](ctx, token, tagsURL)
if err != nil {
return fmt.Errorf("list tags for repo %d: %w", repo.ID, err)

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitLab's .../tags endpoint is paginated. Fetching only the first page means keepLatest will be applied to a partial tag set, so older tags beyond the first page will never be pruned. Consider adding per_page and iterating through all pages before sorting/applying retention, or alternatively repeatedly delete from subsequent pages until the tag count is within keepLatest.

Copilot uses AI. Check for mistakes.
Comment on lines +209 to +213
if err != nil {
return zero, err
}
req.Header.Set("PRIVATE-TOKEN", token)

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All GitLab API calls here send the token as PRIVATE-TOKEN, but resolveCredentials can return a CI_JOB_TOKEN. GitLab expects JOB-TOKEN for job tokens (and PRIVATE-TOKEN/Authorization for PAT/OAuth). As-is, Prune is likely to fail in CI contexts where only CI_JOB_TOKEN is available. Consider returning an auth mode/header from resolveCredentials (or a helper that applies the correct headers) and using it for all API requests.

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +190
if err != nil {
return err
}
req.Header.Set("PRIVATE-TOKEN", token)
resp, err := http.DefaultClient.Do(req)

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tag deletions also always authenticate using PRIVATE-TOKEN. If the token is coming from CI_JOB_TOKEN, GitLab requires JOB-TOKEN instead, so deletions will likely return 401/403 in GitLab CI. Use the correct header depending on credential type (job token vs PAT/OAuth), ideally via a shared helper to keep GET/DELETE consistent.

Copilot uses AI. Check for mistakes.
Comment thread cmd/wfctl/build.go Outdated
Comment on lines +33 to +34
case "audit", "--security-audit":
return runBuildSecurityAudit(rest)

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wfctl build --security-audit cannot work with the current dispatch logic: arguments starting with '-' are treated as flags (see isFlag), so this case is never reached, and the main FlagSet doesn't define a --security-audit flag. Invoking wfctl build --security-audit will fail with an unknown-flag parse error instead of running the audit. Consider either adding a real --security-audit boolean flag to the main wfctl build FlagSet (and invoking the audit before orchestration), or removing the --security-audit alias here and documenting wfctl build audit as the supported entrypoint.

Copilot uses AI. Check for mistakes.
Comment thread cmd/wfctl/build_image.go Outdated

// T33: BuildKit provenance attestation when hardened=true.
if hardened {
if os.Getenv("DOCKER_BUILDKIT") != "1" {

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BuildKit warning is based on the current process environment, but the actual docker command always forces DOCKER_BUILDKIT=1 via cmd.Env. As written, this can emit a warning in non-dry-run executions even though the build will run with BuildKit enabled. Consider gating this warning to dry-run only, or basing it on whether the invoked command will have BuildKit enabled (e.g., the env you set on cmd).

Suggested change
if os.Getenv("DOCKER_BUILDKIT") != "1" {
if dryRun && os.Getenv("DOCKER_BUILDKIT") != "1" {

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +69
for _, f := range findings {
if f.Severity == "WARN" {
return fmt.Errorf("%d build security issue(s) found", len(findings))
}
}

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In --strict mode the returned error message uses len(findings) as the count, but findings can include NOTE-level entries as well as WARN. This can over-report the number of warning issues triggering the non-zero exit. Consider counting only Severity == "WARN" findings for the strict-mode error (and potentially wording the message as warnings).

Suggested change
for _, f := range findings {
if f.Severity == "WARN" {
return fmt.Errorf("%d build security issue(s) found", len(findings))
}
}
warnCount := 0
for _, f := range findings {
if f.Severity == "WARN" {
warnCount++
}
}
if warnCount > 0 {
return fmt.Errorf("%d build security warning(s) found", warnCount)
}

Copilot uses AI. Check for mistakes.
Comment on lines +157 to +159
encodedProject := url.PathEscape(projectPath)
baseURL := "https://gitlab.com"

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GitLab API base URL is hard-coded to https://gitlab.com. This will break Prune for self-managed GitLab instances (where the API host is different), even if the registry path points at a different domain. Consider deriving the API base from configuration (e.g., from cfg.Registry.Path or an explicit api_base_url setting) so the provider works outside gitlab.com.

Copilot uses AI. Check for mistakes.
Comment thread plugins/registry-gitlab/plugin.go Outdated
Comment on lines +160 to +163
// List registry repositories for the project.
repoURL := fmt.Sprintf("%s/api/v4/projects/%s/registry/repositories", baseURL, encodedProject)
repos, err := glGetJSON[[]glRepository](ctx, token, repoURL)
if err != nil {

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitLab's projects/.../registry/repositories endpoint is paginated. Fetching it once without per_page and without following pagination headers can miss repositories for larger projects, causing Prune to leave tags untouched. Consider requesting a larger per_page and/or iterating pages using X-Next-Page/Link headers.

Copilot uses AI. Check for mistakes.
…audit (T34)

Extends the config-level audit with per-target linting:
- Calls builder.SecurityLint() for each typed target (go/nodejs/custom)
- For method:dockerfile containers: scans Dockerfile for USER root
  (critical), missing USER (critical), FROM :latest (warn), ADD URL
  (warn), embedded secrets (critical), base image policy violations (warn)
- Exit code: CRITICAL → always 1; --strict → 1 on any warn
- 9 new tests covering each Dockerfile rule and exit-code semantics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Apr 19, 2026

Copy link
Copy Markdown

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:245: parsing iteration count: invalid syntax
baseline-bench.txt:324054: parsing iteration count: invalid syntax
baseline-bench.txt:631678: parsing iteration count: invalid syntax
baseline-bench.txt:954221: parsing iteration count: invalid syntax
baseline-bench.txt:1273884: parsing iteration count: invalid syntax
baseline-bench.txt:1622878: parsing iteration count: invalid syntax
benchmark-results.txt:245: parsing iteration count: invalid syntax
benchmark-results.txt:329610: parsing iteration count: invalid syntax
benchmark-results.txt:639880: parsing iteration count: invalid syntax
benchmark-results.txt:981380: parsing iteration count: invalid syntax
benchmark-results.txt:1270743: parsing iteration count: invalid syntax
benchmark-results.txt:1580698: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │       sec/op       │    sec/op      vs base              │
InterpreterCreation-4              3.732m ± 178%   3.336m ± 182%       ~ (p=0.818 n=6)
ComponentLoad-4                    3.567m ±   0%   3.650m ±   3%  +2.32% (p=0.002 n=6)
ComponentExecute-4                 1.934µ ±   3%   1.976µ ±   1%       ~ (p=0.093 n=6)
PoolContention/workers-1-4         1.074µ ±   3%   1.099µ ±   2%  +2.33% (p=0.024 n=6)
PoolContention/workers-2-4         1.083µ ±   2%   1.092µ ±   1%       ~ (p=0.221 n=6)
PoolContention/workers-4-4         1.083µ ±   1%   1.101µ ±   1%  +1.66% (p=0.011 n=6)
PoolContention/workers-8-4         1.086µ ±   1%   1.099µ ±   1%  +1.24% (p=0.002 n=6)
PoolContention/workers-16-4        1.113µ ±   1%   1.100µ ±   1%       ~ (p=0.061 n=6)
ComponentLifecycle-4               3.656m ±   1%   3.615m ±   0%  -1.11% (p=0.002 n=6)
SourceValidation-4                 2.308µ ±   0%   2.257µ ±   3%       ~ (p=0.130 n=6)
RegistryConcurrent-4               827.6n ±   4%   798.2n ±   1%  -3.55% (p=0.004 n=6)
LoaderLoadFromString-4             3.603m ±   1%   3.662m ±   1%  +1.63% (p=0.002 n=6)
geomean                            17.76µ          17.65µ         -0.61%

                            │ baseline-bench.txt │        benchmark-results.txt         │
                            │        B/op        │     B/op      vs base                │
InterpreterCreation-4               2.027Mi ± 0%   2.027Mi ± 0%       ~ (p=0.669 n=6)
ComponentLoad-4                     2.180Mi ± 0%   2.180Mi ± 0%       ~ (p=0.786 n=6)
ComponentExecute-4                  1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4         1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                2.183Mi ± 0%   2.183Mi ± 0%       ~ (p=0.736 n=6)
SourceValidation-4                  1.984Ki ± 0%   1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                1.133Ki ± 0%   1.133Ki ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4              2.182Mi ± 0%   2.182Mi ± 0%       ~ (p=1.000 n=6)
geomean                             15.25Ki        15.25Ki       -0.00%
¹ all samples are equal

                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │     allocs/op      │  allocs/op   vs base                │
InterpreterCreation-4                15.68k ± 0%   15.68k ± 0%       ~ (p=1.000 n=6)
ComponentLoad-4                      18.02k ± 0%   18.02k ± 0%       ~ (p=1.000 n=6)
ComponentExecute-4                    25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4           25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                 18.07k ± 0%   18.07k ± 0%       ~ (p=1.000 n=6) ¹
SourceValidation-4                    32.00 ± 0%    32.00 ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                  2.000 ± 0%    2.000 ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4               18.06k ± 0%   18.06k ± 0%       ~ (p=1.000 n=6) ¹
geomean                               183.3         183.3       +0.00%
¹ all samples are equal

pkg: github.com/GoCodeAlone/workflow/middleware
                                  │ baseline-bench.txt │       benchmark-results.txt       │
                                  │       sec/op       │   sec/op     vs base              │
CircuitBreakerDetection-4                  283.9n ± 0%   285.8n ± 6%  +0.69% (p=0.002 n=6)
CircuitBreakerExecution_Success-4          21.50n ± 0%   21.55n ± 1%       ~ (p=0.065 n=6)
CircuitBreakerExecution_Failure-4          65.82n ± 0%   66.22n ± 0%  +0.62% (p=0.002 n=6)
geomean                                    73.78n        74.17n       +0.52%

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │        B/op        │    B/op     vs base                │
CircuitBreakerDetection-4                 144.0 ± 0%     144.0 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │     allocs/op      │ allocs/op   vs base                │
CircuitBreakerDetection-4                 1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │       sec/op       │    sec/op     vs base              │
JQTransform_Simple-4                     879.9n ± 28%   868.8n ± 28%       ~ (p=0.394 n=6)
JQTransform_ObjectConstruction-4         1.423µ ±  1%   1.446µ ±  0%  +1.58% (p=0.002 n=6)
JQTransform_ArraySelect-4                3.269µ ±  0%   3.275µ ±  1%       ~ (p=0.329 n=6)
JQTransform_Complex-4                    37.58µ ±  0%   37.34µ ±  0%  -0.65% (p=0.002 n=6)
JQTransform_Throughput-4                 1.761µ ±  0%   1.765µ ±  0%       ~ (p=0.054 n=6)
SSEPublishDelivery-4                     72.66n ±  0%   72.36n ±  1%  -0.41% (p=0.037 n=6)
geomean                                  1.643µ         1.642µ        -0.05%

                                 │ baseline-bench.txt │        benchmark-results.txt         │
                                 │        B/op        │     B/op      vs base                │
JQTransform_Simple-4                   1.273Ki ± 0%     1.273Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4       1.773Ki ± 0%     1.773Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4              2.625Ki ± 0%     2.625Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                  16.22Ki ± 0%     16.22Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4               1.984Ki ± 0%     1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²                 +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │     allocs/op      │ allocs/op   vs base                │
JQTransform_Simple-4                     10.00 ± 0%     10.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4         15.00 ± 0%     15.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4                30.00 ± 0%     30.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                    324.0 ± 0%     324.0 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4                 17.00 ± 0%     17.00 ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
                                    │ baseline-bench.txt │       benchmark-results.txt       │
                                    │       sec/op       │   sec/op     vs base              │
SchemaValidation_Simple-4                   1.135µ ± 10%   1.127µ ± 6%       ~ (p=0.818 n=6)
SchemaValidation_AllFields-4                1.663µ ±  4%   1.663µ ± 3%       ~ (p=0.937 n=6)
SchemaValidation_FormatValidation-4         1.588µ ±  1%   1.593µ ± 1%       ~ (p=0.513 n=6)
SchemaValidation_ManySchemas-4              1.793µ ±  3%   1.792µ ± 3%       ~ (p=0.937 n=6)
geomean                                     1.522µ         1.520µ       -0.12%

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │        B/op        │    B/op     vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │     allocs/op      │ allocs/op   vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
                                   │ baseline-bench.txt │       benchmark-results.txt        │
                                   │       sec/op       │    sec/op     vs base              │
EventStoreAppend_InMemory-4                1.131µ ± 32%   1.177µ ± 17%       ~ (p=1.000 n=6)
EventStoreAppend_SQLite-4                  1.313m ±  5%   1.377m ±  6%  +4.90% (p=0.015 n=6)
GetTimeline_InMemory/events-10-4           13.66µ ±  2%   13.79µ ±  4%       ~ (p=0.589 n=6)
GetTimeline_InMemory/events-50-4           77.75µ ±  5%   76.11µ ± 21%       ~ (p=0.240 n=6)
GetTimeline_InMemory/events-100-4          121.1µ ± 28%   121.0µ ±  0%       ~ (p=0.699 n=6)
GetTimeline_InMemory/events-500-4          623.8µ ±  3%   621.8µ ±  1%       ~ (p=0.699 n=6)
GetTimeline_InMemory/events-1000-4         1.253m ±  1%   1.275m ±  1%  +1.76% (p=0.002 n=6)
GetTimeline_SQLite/events-10-4             104.1µ ±  0%   108.1µ ±  1%  +3.89% (p=0.002 n=6)
GetTimeline_SQLite/events-50-4             242.1µ ±  0%   249.9µ ±  0%  +3.21% (p=0.002 n=6)
GetTimeline_SQLite/events-100-4            410.6µ ±  2%   429.4µ ±  1%  +4.60% (p=0.002 n=6)
GetTimeline_SQLite/events-500-4            1.749m ±  2%   1.795m ±  0%  +2.64% (p=0.002 n=6)
GetTimeline_SQLite/events-1000-4           3.408m ±  0%   3.501m ±  0%  +2.71% (p=0.002 n=6)
geomean                                    214.0µ         218.6µ        +2.16%

                                   │ baseline-bench.txt │        benchmark-results.txt         │
                                   │        B/op        │     B/op      vs base                │
EventStoreAppend_InMemory-4                  825.5 ± 9%     802.5 ± 8%       ~ (p=0.310 n=6)
EventStoreAppend_SQLite-4                  1.985Ki ± 2%   1.984Ki ± 2%       ~ (p=0.613 n=6)
GetTimeline_InMemory/events-10-4           7.953Ki ± 0%   7.953Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4           46.62Ki ± 0%   46.62Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4          94.48Ki ± 0%   94.48Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4          472.8Ki ± 0%   472.8Ki ± 0%       ~ (p=1.000 n=6)
GetTimeline_InMemory/events-1000-4         944.3Ki ± 0%   944.3Ki ± 0%       ~ (p=1.000 n=6)
GetTimeline_SQLite/events-10-4             16.74Ki ± 0%   16.74Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4             87.14Ki ± 0%   87.14Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4            175.4Ki ± 0%   175.4Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4            846.1Ki ± 0%   846.1Ki ± 0%       ~ (p=1.000 n=6)
GetTimeline_SQLite/events-1000-4           1.639Mi ± 0%   1.639Mi ± 0%       ~ (p=0.472 n=6)
geomean                                    67.59Ki        67.43Ki       -0.24%
¹ all samples are equal

                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │     allocs/op      │  allocs/op   vs base                │
EventStoreAppend_InMemory-4                  7.000 ± 0%    7.000 ± 0%       ~ (p=1.000 n=6) ¹
EventStoreAppend_SQLite-4                    53.00 ± 0%    53.00 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-10-4             125.0 ± 0%    125.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4             653.0 ± 0%    653.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4           1.306k ± 0%   1.306k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4           6.514k ± 0%   6.514k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-1000-4          13.02k ± 0%   13.02k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-10-4               382.0 ± 0%    382.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4              1.852k ± 0%   1.852k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4             3.681k ± 0%   3.681k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4             18.54k ± 0%   18.54k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-1000-4            37.29k ± 0%   37.29k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                     1.162k        1.162k       +0.00%
¹ all samples are equal

Benchmarks run with go test -bench=. -benchmem -count=6.
Regressions ≥ 20% are flagged. Results compared via benchstat.

intel352 and others added 3 commits April 19, 2026 02:05
…en header

a) Add CIRegistry.APIBaseURL for self-managed GitLab instances; default
   to https://gitlab.com when unset.
b/c) Replace glGetJSON with glPaginatedGet that follows X-Next-Page
   headers for both repository and tag listing endpoints.
d) resolveCredentials now returns tokenType ("job"|"private"); use
   JOB-TOKEN header for CI_JOB_TOKEN, PRIVATE-TOKEN for PATs.
   AuthHeaderFor exported for testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… in strict mode

e) Remove unreachable --security-audit alias from build dispatcher; only
   `wfctl build audit` is supported.
f) --strict now counts only WARN findings (not NOTE) when deciding exit
   code. Error message reports warn count, not total finding count.
   Add test asserting NOTE-only findings don't trigger --strict exit 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The DOCKER_BUILDKIT warning is misleading in live mode because cmd.Env
already forces DOCKER_BUILDKIT=1 on the docker subprocess. Only emit
the warning during dry-run when the env truly won't be set. Add test
asserting the warning appears in dry-run and doesn't cause false
positives in live builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 19, 2026 06:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Comment on lines +287 to +330
hasUser := false
scanner := bufio.NewScanner(strings.NewReader(string(data)))
lineNum := 0
for scanner.Scan() {
lineNum++
line := strings.TrimSpace(scanner.Text())

if reUserRoot.MatchString(line) {
addDF("CRITICAL", "USER root detected — container will run as root", lineNum)
}
if reUserAny.MatchString(line) {
hasUser = true
}
if reFromLatest.MatchString(line) {
addDF("WARN", "FROM uses :latest tag — pin to a digest or explicit version for reproducibility", lineNum)
}
if reAddURL.MatchString(line) {
addDF("WARN", "ADD with URL is untrusted — use RUN curl/wget with checksum verification instead", lineNum)
}
if reEmbeddedSecret.MatchString(line) {
addDF("CRITICAL", fmt.Sprintf("possible embedded secret in line %d — use BuildKit secrets (--secret) instead", lineNum), lineNum)
}

// Base image policy.
if len(allowPrefixes) > 0 && reFromImage.MatchString(line) {
m := reFromImage.FindStringSubmatch(line)
if len(m) > 1 && m[1] != "scratch" {
img := m[1]
if !matchesAnyPrefix(img, allowPrefixes) {
addDF("WARN", fmt.Sprintf("base image %q does not match allow_prefixes policy %v", img, allowPrefixes), lineNum)
}
}
}
}

if !hasUser {
findings = append(findings, buildAuditFinding{
Severity: "CRITICAL",
Check: checkName,
Message: "no USER directive found — container will run as root by default",
File: dfPath,
Line: 0,
})
}

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dockerfile linting currently treats any USER directive anywhere in the file as sufficient. In multi-stage Dockerfiles, a USER in an earlier stage can cause a false negative if the final stage omits USER (defaulting back to root). Consider tracking USER per stage by resetting hasUser on each FROM, and only enforcing the USER rule (and USER root) on the final stage.

Copilot uses AI. Check for mistakes.
}
}
}
}

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lintDockerfile doesn’t check scanner.Err() after the scan loop. If the Dockerfile contains very long lines (bufio.Scanner token limit) or an I/O error occurs, the scan can terminate early and findings may be incomplete without any signal. Add an if err := scanner.Err(); err != nil { ... } path (likely a WARN/CRITICAL finding) after the loop.

Suggested change
}
}
if err := scanner.Err(); err != nil {
addDF("WARN", fmt.Sprintf("failed to fully scan Dockerfile: %v", err), 0)
}

Copilot uses AI. Check for mistakes.
Comment on lines +154 to +176
// TestGitLabProvider_Prune_SelfManaged checks dry-run mentions the custom API base URL.
func TestGitLabProvider_Prune_SelfManaged(t *testing.T) {
p := registrygitlab.New()
reg := config.CIRegistry{
Name: "self-managed",
Type: "gitlab",
Path: "registry.example.com/myorg/myproject",
APIBaseURL: "https://gitlab.example.com",
Auth: &config.CIRegistryAuth{Env: "GITLAB_TOKEN"},
Retention: &config.CIRegistryRetention{KeepLatest: 3},
}
t.Setenv("GITLAB_TOKEN", "glpat-selfmanaged-token")

var buf bytes.Buffer
ctx := registry.NewContext(t.Context(), &buf, true)
if err := p.Prune(ctx, registry.ProviderConfig{Registry: reg}); err != nil {
t.Fatalf("Prune dry-run self-managed: %v", err)
}

out := buf.String()
if !strings.Contains(out, "myorg/myproject") {
t.Errorf("expected project path in dry-run output, got: %q", out)
}

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test comment says the dry-run output mentions the custom API base URL, but the assertions only check for the project path (and the provider’s dry-run output doesn’t currently include APIBaseURL). Either update the comment to match what’s actually asserted, or extend the assertion/output to include the API base URL.

Copilot uses AI. Check for mistakes.
func runBuildAuditChecks(cfgPath, workDir string) []buildAuditFinding {
cfg, err := config.LoadFromFile(cfgPath)
if err != nil {
return []buildAuditFinding{{Severity: "WARN", Check: "config", Message: fmt.Sprintf("failed to load config: %v", err)}}

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runBuildAuditChecks converts config load failures into a WARN finding. As a result, wfctl build audit can exit 0 (when not --strict) even though the audit never ran. Consider returning an error from runBuildSecurityAudit when the config can't be loaded (or mark this finding as CRITICAL so it always forces a non-zero exit).

Suggested change
return []buildAuditFinding{{Severity: "WARN", Check: "config", Message: fmt.Sprintf("failed to load config: %v", err)}}
return []buildAuditFinding{{Severity: "CRITICAL", Check: "config", Message: fmt.Sprintf("failed to load config: %v", err)}}

Copilot uses AI. Check for mistakes.
return fmt.Sprintf("[%s] %s: %s", f.Severity, f.Check, f.Message)
}

// runBuildSecurityAudit implements `wfctl build audit` and `wfctl build --security-audit`.

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says this implements wfctl build --security-audit, but there’s no --security-audit flag wired into wfctl build in this PR (only the build audit subcommand is added). Either implement the flag and route it here, or update the comment to avoid advertising an unsupported CLI option.

Suggested change
// runBuildSecurityAudit implements `wfctl build audit` and `wfctl build --security-audit`.
// runBuildSecurityAudit implements `wfctl build audit`.

Copilot uses AI. Check for mistakes.
@intel352 intel352 merged commit 90257d9 into main Apr 19, 2026
22 checks passed
@intel352 intel352 deleted the feat/v0.14.1-security-audit branch April 19, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants